package joc.internal.transformer.helper;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.ParameterAnnotationsAttribute;
import javassist.bytecode.annotation.Annotation;
import joc.JOCEnabled;

import org.apache.log4j.Logger;

public class AnnotationInjector {
	private CtClass targetClass;
	private ClassPool pool;
	private static final Logger logger = Logger.getLogger(AnnotationInjector.class);

	public AnnotationInjector(CtClass targetClass, ClassPool pool) {
		this.targetClass = targetClass;
		this.pool = pool;
	}

	public void injectExternalContract(CtClass contractClass) throws NotFoundException {
		addClassAnnotations(contractClass);
		for (CtField contractField : contractClass.getFields()) {
			CtField targetField = targetClass.getField(contractField.getName());
			addFieldAnnotations(contractField, targetField);
		}
		for (CtBehavior contractBehavior : contractClass.getDeclaredBehaviors()) {
			CtBehavior targetBehavior = null;
			if (contractBehavior instanceof CtConstructor) {
				try {
					targetBehavior = targetClass.getDeclaredConstructor(contractBehavior.getParameterTypes());
				} catch (NotFoundException e) {
					// default constructor should be ignored
					if (contractBehavior.getParameterTypes().length > 0) {
						throw e;
					}
				}
			} else {
				targetBehavior = targetClass.getDeclaredMethod(contractBehavior.getName(),
						contractBehavior.getParameterTypes());
			}
			addBehaviorAnnotations(contractBehavior, targetBehavior);
			addBehaviorParameterAnnotations(contractBehavior, targetBehavior);
		}
		targetClass.getClassFile().setVersionToJava5();
		targetClass.rebuildClassFile();
	}

	private void addBehaviorParameterAnnotations(CtBehavior contractBehavior, CtBehavior targetBehavior) {
		ParameterAnnotationsAttribute contractAttribute = (ParameterAnnotationsAttribute) contractBehavior
				.getMethodInfo().getAttribute(ParameterAnnotationsAttribute.invisibleTag);
		if (contractAttribute == null) {
			return;
		}
		ParameterAnnotationsAttribute targetAttribute = (ParameterAnnotationsAttribute) targetBehavior.getMethodInfo()
				.getAttribute(ParameterAnnotationsAttribute.invisibleTag);
		if (targetAttribute == null) {
			targetAttribute = cloneParameterAttribute(contractAttribute, targetBehavior.getMethodInfo().getConstPool());
			targetBehavior.getMethodInfo().addAttribute(targetAttribute);
		} else {
			copyParameterAnnotations(contractAttribute, targetAttribute);
		}

	}

	private void copyParameterAnnotations(ParameterAnnotationsAttribute contractAttribute,
			ParameterAnnotationsAttribute targetAttribute) {
		int numParameters = contractAttribute.getAnnotations().length;
		Annotation[][] newTargetAnnotations = new Annotation[numParameters][];
		for (int i = 0; i < numParameters; i++) {
			logger.info("adding contract from parameter " + (i + 1));
			Annotation[] contractAnnotations = contractAttribute.getAnnotations()[i];
			if (contractAnnotations != null) {
				Annotation[] targetAnnotations = targetAttribute.getAnnotations()[i];
				if (targetAnnotations == null) {
					newTargetAnnotations[i] = contractAnnotations;
				} else {
					List<Annotation> annotations = new ArrayList<Annotation>(Arrays.asList(targetAnnotations));
					annotations.addAll(Arrays.asList(contractAnnotations));
					newTargetAnnotations[i] = annotations.toArray(new Annotation[] {});
				}
			}
		}
		targetAttribute.setAnnotations(newTargetAnnotations);
	}

	private void addBehaviorAnnotations(CtBehavior contractBehavior, CtBehavior targetBehavior) {
		logger.info("adding contracts from behavior " + contractBehavior.getName());
		AnnotationsAttribute contractAttribute = (AnnotationsAttribute) contractBehavior.getMethodInfo().getAttribute(
				AnnotationsAttribute.invisibleTag);
		if (contractAttribute == null) {
			return;
		}
		AnnotationsAttribute targetAttribute = (AnnotationsAttribute) targetBehavior.getMethodInfo().getAttribute(
				AnnotationsAttribute.invisibleTag);
		if (targetAttribute == null) {
			targetAttribute = cloneAttribute(contractAttribute, targetBehavior.getMethodInfo().getConstPool());
			targetBehavior.getMethodInfo().addAttribute(targetAttribute);
		} else {
			copyAnnotations(contractAttribute, targetAttribute);
		}
	}

	private void addBehaviorAnnotation(CtBehavior targetBehavior, Class<?> annotationClass) throws NotFoundException {
		AnnotationsAttribute targetAttribute = (AnnotationsAttribute) targetBehavior.getMethodInfo().getAttribute(
				AnnotationsAttribute.invisibleTag);
		if (targetAttribute == null) {
			targetAttribute = new AnnotationsAttribute(targetBehavior.getMethodInfo().getConstPool(),
					AnnotationsAttribute.invisibleTag);
			targetBehavior.getMethodInfo().addAttribute(targetAttribute);
		}
		targetAttribute.addAnnotation(new javassist.bytecode.annotation.Annotation(targetBehavior.getMethodInfo()
				.getConstPool(), pool.get(annotationClass.getName())));
	}

	private void addFieldAnnotations(CtField contractField, CtField targetField) {
		logger.info("adding contracts from field " + contractField.getName());
		AnnotationsAttribute contractAttribute = (AnnotationsAttribute) contractField.getFieldInfo().getAttribute(
				AnnotationsAttribute.invisibleTag);
		if (contractAttribute == null) {
			return;
		}
		AnnotationsAttribute targetAttribute = (AnnotationsAttribute) targetField.getFieldInfo().getAttribute(
				AnnotationsAttribute.invisibleTag);
		if (targetAttribute == null) {
			targetAttribute = cloneAttribute(contractAttribute, targetField.getFieldInfo().getConstPool());
			targetField.getFieldInfo().addAttribute(targetAttribute);
		} else {
			copyAnnotations(contractAttribute, targetAttribute);
		}
	}

	private void addClassAnnotations(CtClass contractClass) throws NotFoundException {
		logger.info("adding contracts from class " + contractClass.getName());
		AnnotationsAttribute contractAttribute = (AnnotationsAttribute) contractClass.getClassFile().getAttribute(
				AnnotationsAttribute.invisibleTag);
		if (contractAttribute == null) {
			return;
		}
		AnnotationsAttribute targetAttribute = (AnnotationsAttribute) targetClass.getClassFile().getAttribute(
				AnnotationsAttribute.invisibleTag);
		if (targetAttribute == null) {
			targetAttribute = cloneAttribute(contractAttribute, targetClass.getClassFile().getConstPool());
			targetClass.getClassFile().addAttribute(targetAttribute);
		} else {
			copyAnnotations(contractAttribute, targetAttribute);
		}
		addJOCEnabledAnnotationToAttribute(targetAttribute);
	}

	public void addJOCEnabledAnnotation() throws NotFoundException {
		AnnotationsAttribute targetAttribute = (AnnotationsAttribute) targetClass.getClassFile().getAttribute(
				AnnotationsAttribute.invisibleTag);
		if (targetAttribute == null) {
			targetAttribute = new AnnotationsAttribute(targetClass.getClassFile().getConstPool(),
					AnnotationsAttribute.invisibleTag);
			targetClass.getClassFile().addAttribute(targetAttribute);
		}
		addJOCEnabledAnnotationToAttribute(targetAttribute);
	}

	private void addJOCEnabledAnnotationToAttribute(AnnotationsAttribute targetAttribute) throws NotFoundException {
		targetAttribute.addAnnotation(new javassist.bytecode.annotation.Annotation(targetClass.getClassFile()
				.getConstPool(), pool.get(JOCEnabled.class.getName())));
	}

	private AnnotationsAttribute cloneAttribute(AnnotationsAttribute contractAttribute, ConstPool constPool) {
		logger.info("adding all contract annotations");
		return (AnnotationsAttribute) contractAttribute.copy(constPool, Collections.EMPTY_MAP);
	}

	private ParameterAnnotationsAttribute cloneParameterAttribute(ParameterAnnotationsAttribute contractAttribute,
			ConstPool constPool) {
		logger.info("adding all contract annotations from parameters");
		return (ParameterAnnotationsAttribute) contractAttribute.copy(constPool, Collections.EMPTY_MAP);
	}

	private void copyAnnotations(AnnotationsAttribute contractAttribute, AnnotationsAttribute targetAttribute) {
		for (javassist.bytecode.annotation.Annotation contractAnnotation : contractAttribute.getAnnotations()) {
			logger.info("adding contract annotation: " + contractAnnotation.getTypeName());
			targetAttribute.addAnnotation(contractAnnotation);
		}
	}

	public void injectImplicitContracts(List<ImplicitContract> enabledImplicitContracts) throws NotFoundException {
		for (ImplicitContract implicitContract : enabledImplicitContracts) {
			for (CtMethod affectedMethod : implicitContract.getAffectedMethods(pool, targetClass)) {
				logger.info("injecting contract " + implicitContract.getContractAnnotation().getName()
						+ " into method " + affectedMethod.getLongName());
				addBehaviorAnnotation(affectedMethod, implicitContract.getContractAnnotation());
			}
		}
	}
}
